# GF_PY312_CLASS_Fuzzy_Match.py
# Create by GF 2025-02-26 20:34

import jieba

class Fuzzy_Match():

    def __init__(self):

        # 占位变量
        self.PLACEHOLDER = None

    def Fuzzy_Match_String_Preprocess(self, ArgX:str) -> str:

        # 引用函数说明:
        # str.maketrans(): 创建字符映射表
        # - str.maketrans() 第 1 个字符串参数: 需要被替换的全角字符 (如 "Ａ, Ｂ, １, ２")
        # - str.maketrans() 第 2 个字符串参数: 对应的替换为半角字符 (如 "A, B, 1, 2")
        # s.translate(): 根据映射表执行字符替换
        #
        # 核心目标:
        # 将全角字符转换为半角：
        # - 全角英文字母 -> 半角英文字母
        # - 全角数字     -> 半角数字
        #
        # Example:
        # >>> orig_str = "Ｈｅｌｌｏ！１２３全角ＴＥＳＴ"
        # >>> Fuzzy_Match_String_Preprocess(orig_str)
        # "Hello！123全角TEST"

        # 全角转半角 + 大小写折叠 + 移除空格
        StrX = str(ArgX).translate(
                   str.maketrans(
                       "ＡＢＣＤＥＦＧＨＩＪＫＬＭＮＯＰＱＲＳＴＵＶＷＸＹＺａｂｃｄｅｆｇｈｉｊｋｌｍｎｏｐｑｒｓｔｕｖｗｘｙｚ０１２３４５６７８９",
                       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                   )
               )
        # ..........................................
        Result = StrX.lower()
        Result = Result.replace(' ', '')
        # ..........................................
        return Result

    def Max_Length_String_for_2_String(self, Str_1:str, Str_2:str) -> str:

        # Example:
        # >>> Str_1 = "Hello"
        # >>> Str_2 = "Hello World"
        # >>> Str_3 = "Hello Apple"
        # >>> Max_Length_String_for_2_String(Str_1, Str_2)
        # "Hello World"
        # >>> Max_Length_String_for_2_String(Str_2, Str_3)
        # "Hello World"

        Str_1_Length = len(Str_1)
        Str_2_Length = len(Str_2)

        if (Str_1_Length - Str_2_Length)  > 0:
            return Str_1

        if (Str_1_Length - Str_2_Length)  < 0:
            return Str_2

        if (Str_1_Length - Str_2_Length) == 0:
            return Str_1

    def Min_Length_String_for_2_String(self, Str_1:str, Str_2:str) -> str:

        # Example:
        # >>> Str_1 = "Yes"
        # >>> Str_2 = "No Commander"
        # >>> Str_3 = "He disagrees"
        # >>> Min_Length_String_for_2_String(Str_1, Str_2)
        # "Yes"
        # >>> Min_Length_String_for_2_String(Str_2, Str_3)
        # "No Commander"

        Str_1_Length = len(Str_1)
        Str_2_Length = len(Str_2)

        if (Str_1_Length - Str_2_Length)  < 0:
            return Str_1

        if (Str_1_Length - Str_2_Length)  > 0:
            return Str_2

        if (Str_1_Length - Str_2_Length) == 0:
            return Str_1

    def Fuzzy_Match_by_Same_Word_Num_with_jieba(self, Str_1:str, Str_2:str) -> float:

        # Example:
        # >>> Str_1 = "我来到北京清华大学"
        # >>> Str_2 = "我来到四川大学"
        # >>> Fuzzy_Matching_Similarity_by_jieba(Str_1, Str_2)
        # 0.5
        #
        # Caution:
        # jieba 分词器对中文支持更好, 对英文的支持不如 nltk

        # jieba.cut("自然语言处理", cut_all=False)
        # 其中 cut_all=False 为精确模式 (将文本精确地切分成词语)
        Str_1_List = list( jieba.cut(Str_1, cut_all=False) )
        Str_2_List = list( jieba.cut(Str_2, cut_all=False) )

        # 分词及词数相似度计算试验 (jieba 0.42.1):
        #
        # >>> A = "语言  处理"      # 包含 2 个空格 (' ') 以进行强制分词
        # >>> B = "自然语言 处理"   # 包含 1 个空格 (' ') 以进行强制分词
        # >>> list(jieba.cut(A, cut_all=False))  # 字符串长度为 6, 分词数为 4 个词
        # ['语言', ' ', ' ', '理处']
        # >>> list(jieba.cut(B, cut_all=False))  # 字符串长度为 7, 分词数为 3 个词
        # ['自然语言', ' ', '处理']
        #
        # 如果 A 和 B 计算相似度, 按 A 包含的词在 B 包含的词中出现的次数 / B 的总词数, 则不准确,
        # 结果为 {"语言":0, " ":1, " ":1, "理处":1}, 结果 = 3 / 3 = 1.0
        # 如果 A 和 B 计算相似度, 按 B 包含的词在 A 包含的词中出现的次数 / A 的总词数, 则更准确,
        # 结果为 {"自然语言":0, " ":1, "处理":1}, 结果 = 2 / 4 = 0.5
        #
        # 所以如果要按词数计算两个字符串的相似度, 应在分词后, 取词数更多的做分母

        More_Words = Str_1_List if len(Str_1_List) >= len(Str_2_List) else Str_2_List
        Less_Words = Str_2_List if len(Str_1_List) >= len(Str_2_List) else Str_1_List

        Times_of_Iter = len(Less_Words) - 1
        Times_of_Same = 0

        i = 0
        while (i <= Times_of_Iter):
            if (Less_Words[i] in More_Words):
                Times_of_Same = Times_of_Same + 1
            i = i + 1

        Similarity = Times_of_Same / len(More_Words)

        return Similarity

    def Fuzzy_Match_by_Char_Num_Score(self, Text:str, Keys:str) -> float:

        # 模糊匹配 - 字符数量评分法 (返回匹配度评分)
        #
        # Args:
        # - Text: 待匹配的原始文本
        # - Keys: 搜索关键词
        # Returns:
        # - float: 匹配度 (0.0 - 1.0), 越接近 1 表示匹配度越高
        #
        # 匹配方式:
        # 遍历 Keys 中的每个字符,
        # 只要能在 Text 中找到 (按顺序), 就累加匹配数 m,
        # 若找不到, 则跳过该字符继续处理后续字符。
        #
        # 结果计算:
        # 最终得分基于匹配字符数 m 占 n 的比例, 直接反映匹配的字符比例。
        # n 取值为 Text 和 Keys 中字符数最多的那个。

        Processed_Text = self.Fuzzy_Match_String_Preprocess(Text)
        Processed_Keys = self.Fuzzy_Match_String_Preprocess(Keys)

        # ..........................................

        # 空的 Keys 匹配所有内容
        if not Processed_Keys:
            return 1.0

        # ..........................................

        m = 0  # 实际匹配的字符数
        curr_posi = 0
        for c in Processed_Keys:
            pos = Processed_Text.find(c, curr_posi)
            if pos != -1:
                m += 1
                curr_posi = pos + 1  # 确保顺序查找下一个字符

        k = len(Processed_Keys)
        if k == 0:
            return 0.0

        # 如果 Processed_Text 长度更长则 n 取 Processed_Text 的长度,
        # 如果 Processed_Keys 长度更长则 n 取 Processed_Keys 的长度。
        t = len(Processed_Text)
        n = t if t > k else k

        # 计算匹配字符数的比例
        match_ratio = m / n

        # ..........................................

        return round(match_ratio, 4)

    def Fuzzy_Match_by_Char_Gap_Score(self, Text:str, Keys:str) -> float:

        # 模糊匹配 - 字符间隔评分法 (返回匹配度评分)
        #
        # Args:
        # - Text: 待匹配的原始文本
        # - Keys: 搜索关键词
        # Returns:
        # - float: 匹配度 (0.0 - 1.0), 越接近 1 表示匹配度越高
        #
        # 评分维度:
        # 基础分 (70% 权重): 基于平均字符间隔计算, 连续字符得 1 分, 每增加一个间隔字符扣分。
        # 长度分 (30% 权重): 匹配字符数占文本长度的比例, 鼓励匹配更长内容。
        #
        # 字符间隔计算:
        # 原始文本: "a_bc", 关键词: "abc"
        # 字符位置: [0, 2, 3]
        # 间隔计算: (2 - 0 - 1) + (3 - 2 - 1) = 1 + 0 = 1
        # 平均间隔: 1 / 2 = 0.5
        # 基础分值: 1 / (1 + 0.5) = 0.666

        Processed_Text = self.Fuzzy_Match_String_Preprocess(Text)
        Processed_Keys = self.Fuzzy_Match_String_Preprocess(Keys)

        # ..........................................

        # 空的 Keys 匹配所有内容
        if not Processed_Keys:
            return 1.0

        # ..........................................

        # 顺序查找字符位置
        # 遇到无法找到的字符时直接返回 0, 所以部分匹配无法得到分数
        positions = []
        curr_posi = 0
        for c in Processed_Keys:
            # 返回 Processed_Text 中从位置 curr_posi 到末尾找到字符 c 的最小索引
            pos = Processed_Text.find(c, curr_posi)
            if pos == -1:
                return 0.0
            positions.append(pos)
            curr_posi = pos + 1  # 确保顺序查找下一个字符

        # 计算匹配质量指标
        n = len(positions)
        if n == 1:
            return 1.0  # 单个字符完全匹配

        # 计算有效间隔 (排除字符本身占位)
        total_gap = 0
        for i in range(1, n):
            total_gap += (positions[i] - positions[i-1] - 1)

        # 计算平均间隔
        avg_gap = total_gap / (n-1)

        # 计算基础评分 (间隔越小越好)
        base_score = 1 / (1 + avg_gap)

        # 加入长度修正系数 (匹配越长分值越高)
        length_factor = min(len(Processed_Keys) / len(Processed_Text), 1.0)

        # 综合评分公式
        final_score = (base_score * 0.7) + (length_factor * 0.3)

        # ..........................................

        return round(final_score, 4)

    def Fuzzy_Match_by_Char_Num_Score_Auto(self, Str_1:str, Str_2:str) -> float:

        # 模糊匹配 - 字符数量评分法 - 自动 (返回匹配度评分)
        # ..........................................
        # 两次调用同类型函数, 在第 2 次调用时交换 Str_1 和 Str_2 的位置,
        # 以免 Text 和 Keys 的值是同一个字符串 (String),
        # 因为 Str_1 和 Str_2 长度相等时, 默认返回传入的第 1 个字符串
        Text = self.Max_Length_String_for_2_String(Str_1, Str_2)
        Keys = self.Min_Length_String_for_2_String(Str_2, Str_1)
        # ..........................................
        Result = self.Fuzzy_Match_by_Char_Num_Score(Text, Keys)
        # ..........................................
        return Result

    def Fuzzy_Match_by_Char_Gap_Score_with_Threshold(self, Text:str, Keys:str, Threshold=0.5) -> float:

        # 模糊匹配 - 带阈值的间隔分值法 (返回匹配度评分)
        # ..........................................
        return self.Fuzzy_Match_by_Char_Gap_Score(Text, Keys) >= Threshold

# EOF Signed by GF.
